/*
 * Copyright 2014 OGIS-RI All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package jp.co.ogis_ri.optaplanner.core.api.solver;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.optaplanner.core.api.domain.solution.Solution;
import org.optaplanner.core.api.score.Score;
import org.optaplanner.core.api.solver.SolverFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Solutionを、異なる乱数シードを用いて計算し、最もスコアの良いものを取得するためのSolver
 * 
 */
public class MultiSolver {

	protected final transient Logger logger = LoggerFactory.getLogger(this
			.getClass());

	Solution bestSolution;

	SolverFactory solverFactory;

	Solution unsolvedSolution;

	int calculationCount;

	int threadCount;

	ExecutorService executorService;

	Collection<SolutionTask> tasks;

	Object lockObject = new Object();

	boolean isTerminateEarly = false;

	public MultiSolver(final SolverFactory solverFactory,
			final Solution unsolvedSolution, final int calculationCount,
			final ExecutorService executorService) {

		this.solverFactory = solverFactory;
		this.unsolvedSolution = unsolvedSolution;
		this.calculationCount = calculationCount;
		this.executorService = executorService;
	}

	/**
	 * 
	 * @param solverFactory
	 *            solverFactory
	 * @param unsolvedSolution
	 *            unsolved Solution
	 * @param calculationCount
	 *            計算回数。
	 * @param threadPool
	 *            スレッドの作成数（最大でも コア数ー１ までに自動動的に制限します）
	 */
	public MultiSolver(final SolverFactory solverFactory,
			final Solution unsolvedSolution, final int calculationCount,
			final int threadCount) {

		this.solverFactory = solverFactory;
		this.unsolvedSolution = unsolvedSolution;
		this.calculationCount = calculationCount;

		// Max threadCount is (core count)-1
		if (Runtime.getRuntime().availableProcessors() - 1 < threadCount) {
			this.threadCount = Runtime.getRuntime().availableProcessors() - 1;
		} else {
			this.threadCount = threadCount;
		}
	}

	/**
	 * 乱数シードを変更して、calculationCountで指定した回数の計算を行う。
	 * threadPoolで指定したスレッドで並列計算し、すべての計算が完了するまで待ちます
	 */
	public void solve() {

		synchronized (this.lockObject) {
			if (this.isTerminateEarly) {
				return;
			}

			Random random = new Random(new Date().getTime());

			this.logger.debug("AvailableProcessors:"
					+ Runtime.getRuntime().availableProcessors()
					+ " ThreadPool:" + this.threadCount);

			if (this.executorService == null) {
				this.executorService = Executors
						.newFixedThreadPool(this.threadCount);
			}

			// タスク一覧を作成
			this.tasks = new ArrayList<SolutionTask>();

			for (int i = 0; i < this.calculationCount; i++) {
				this.tasks.add(new SolutionTask(this.solverFactory,
						this.unsolvedSolution, random.nextLong(), Integer
								.toString(i)));
			}
		}

		// タスクを実行
		try {
			List<Future<Solution>> results = this.executorService
					.invokeAll(this.tasks);

			// スレッドプールを破棄
			this.executorService.shutdown();

			// もっともよい結果を取得する
			Score bestSoftSocre = null;

			for (Future<Solution> future : results) {
				try {
					if (future.get() != null) {
						Score score = future.get().getScore();

						this.logger.debug(score.toString());

						if (bestSoftSocre == null
								|| bestSoftSocre.compareTo(score) < 0) {
							bestSoftSocre = score;
							this.bestSolution = future.get();
						}
					}
				} catch (ExecutionException e) {
					this.logger.error("Solve error:", e);
				}
			}

			if (this.bestSolution == null) {
				this.logger.warn("BestSolution Score:null");
			} else {
				this.logger.info("BestSolution Score:"
						+ this.bestSolution.getScore().toString());
			}

		} catch (InterruptedException e1) {
			throw new RuntimeException(e1);
		}

	}

	/**
	 * 
	 * @return
	 */
	public Solution getBestSolution() {
		return this.bestSolution;
	}

	/**
	 * 
	 * @return
	 */
	public void terminateEarly() {

		synchronized (this.lockObject) {
			this.isTerminateEarly = true;

			if (this.tasks != null) {
				for (SolutionTask solutionTask : this.tasks) {
					solutionTask.terminateEarly();
				}
			}
		}
	}

}
